Skip to content

πŸ› fix(deploy): reflect intercepted 403/404 from the ingress error backend instead of answering 503 - #17432

Merged
hivecommons-hive[bot] merged 3 commits into
v5from
fix/error-pages-reflect-404
Nov 9, 2026
Merged

hivecommons-hive[bot] merged 3 commits into
v5from
fix/error-pages-reflect-404

Conversation

@clubanderson

Copy link
Copy Markdown
Member

Fixes #17431

The OKE ingress-nginx controller had a global custom-http-errors: "403,404,502,503,504"; the hub Ingress inherited it, and hive-error-pages had no 403/404 branch, so every missing hub URL was answered as a 503 "Hive is restarting" page. Google Search Console reported Soft 404 on hivecommons.dev.

Live fix already applied (global list β†’ 502,503,504; hub unknown paths now 404). This PR is the safety net in the backend:

  • notfound.html / notfound.json documents (no spinner, no refresh, noindex)
  • error_page 403 404 /notfound.$hive_error_ext
  • location / reflects 403 and 404 instead of falling through to 503
  • header comment pins the global list and explains why

No Go changes; changelog fragment included.

…kend instead of answering 503

The controller's global custom-http-errors list on the hub's cluster included
403 and 404, and the hive-error-pages backend had no branch for either, so
every unknown URL on hive.hivecommons.dev was served as a 503 "Hive is
restarting" page with a meta-refresh. Search Console flagged it as Soft 404.

Add notfound.html/notfound.json, route 403/404 to them, reflect the status in
the location / block, and document that the global list must stay 502,503,504.

Fixes #17431

Signed-off-by: Andrew Anderson <andy@clubanderson.com>
@kubestellar-prow kubestellar-prow Bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Nov 9, 2026
@kubestellar-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign hanthor for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubestellar-prow kubestellar-prow Bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Open PR overlap check

Base branch: v5

Conflicts are checked with every PR merged onto the current tip of v5, so a PR that only needs a rebase is not reported as conflicting with its siblings.

No conflicting open PRs or same-file overlaps were found.

@hivecommons-hive

Copy link
Copy Markdown
Contributor
eb92f68

ℹ️ Sentinel notice β€” informational; author is trusted (owner), merge is not blocked

Hive flagged this PR (author @clubanderson, head eb92f68c740e) because it matches behaviors that can override security controls, escalate privileges or damage the codebase. This is a heuristic, not an accusation β€” a maintainer should confirm the change is intended before it merges.

  • sensitive_path β€” changes 1 sensitive path(s) (Touches a sensitive path (OWNERS, workflows, policies, hive config, security docs, …))
    • src/deploy/k8s/error-pages.yaml

Hive did not add the sentinel-alert label because this author is trusted. The finding remains recorded for audit and the dashboard Security tab; set sentinel.trusted_authors_block: true to require the blocking label for trusted authors too.

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Quality note: this is a behaviour change to the error backend, and src/pkg/hub/error_pages_backend_test.go already pins its siblings (#4241 return 5xx, JSON-for-API, healthz). The 403/404 branch has no pin, so the next edit to default.conf can drop error_page 403 404 and nothing goes red β€” the Soft-404 would come back silently.

A test in the same file, using the existing helpers, that fails on v5 and passes with this PR:

// TestErrorBackendReflects403And404 pins #17431: an intercepted 403/404 must be
// answered with that status and a document that tells no one to "retry
// shortly" β€” no auto-refresh, and noindex so search engines do not record a
// Soft 404 against a healthy hub.
func TestErrorBackendReflects403And404(t *testing.T) {
	docs, conf := errorPagesConfigMaps(t)
	directives := stripNginxComments(conf["default.conf"])

	for _, code := range []string{"403", "404"} {
		if !regexp.MustCompile(`return\s+` + code + `\b`).MatchString(directives) {
			t.Errorf("default.conf never returns %s, so an intercepted %s falls through to the 503 page (#17431)", code, code)
		}
	}
	if !regexp.MustCompile(`error_page\s+403\s+404\s+/notfound\.`).MatchString(directives) {
		t.Error("default.conf does not route 403/404 to the notfound document (#17431)")
	}

	body, ok := docs["notfound.json"]
	if !ok {
		t.Fatal("hive-error-pages ConfigMap has no notfound.json for API callers")
	}
	var parsed map[string]any
	if err := json.Unmarshal([]byte(body), &parsed); err != nil {
		t.Fatalf("notfound.json is not valid JSON: %v", err)
	}
	if parsed["ok"] != false {
		t.Errorf(`notfound.json "ok" = %v, want false`, parsed["ok"])
	}

	html, ok := docs["notfound.html"]
	if !ok {
		t.Fatal("hive-error-pages ConfigMap has no notfound.html")
	}
	lower := strings.ToLower(html)
	if !strings.Contains(lower, `name="robots" content="noindex"`) {
		t.Error("notfound.html is indexable; Search Console will record a Soft 404 again (#17431)")
	}
	if strings.Contains(lower, "http-equiv=\"refresh\"") || strings.Contains(lower, "settimeout") || strings.Contains(lower, "location.reload") {
		t.Error("notfound.html auto-refreshes; a missing page is not a 'retry shortly' state")
	}
}

No push from this lane (your branch) β€” adding it to this PR keeps the pin and the fix in one commit.


🐝 Hive Agent: quality | Instance: hosted-available-oke-11-placeholder-r05x | SHA: unknown

β€” hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the full src/deploy/k8s/error-pages.yaml at the v5 base alongside the diff. The nginx wiring is consistent with the existing 401 path and I found no defect:

  • hive-error-pages ConfigMap is mounted whole at /usr/share/nginx/html (error-pages.yaml:216-218, 243-244), so the new notfound.html/notfound.json keys become servable files without a volume items: change.
  • error_page 403 404 /notfound.$hive_error_ext; sits next to the existing error_page 401 /unauthorized.$hive_error_ext;, and $hive_error_ext is already selected by X-Original-URI (/api/ β†’ json) then X-Format (error-pages.yaml:148-155), so API callers get the JSON body and browsers the HTML, same as today for 401.
  • The two if ($http_x_code = ...) { return ...; } lines mirror the 401/500/502/504 ones, so a 404 no longer falls through to the return 503 default.
  • Body and diff agree: no Go change, changelog fragment present, and the header comment pins the global custom-http-errors list to 502,503,504.

One semantic note, not a defect: a 403 is now answered with "not found" copy ({"ok":false,"error":"not found",…} / "Page not found"). That is a reasonable choice for a public hub, but if 403 ever does get intercepted β€” e.g. an API authorization denial behind an Ingress whose list includes 403 β€” a client keying off error will see "not found" rather than "forbidden". If that matters, a separate forbidden.json is a small follow-up.

This touches a deploy manifest in src/deploy/k8s/, which is a trust surface under my review policy, so I am recording this for a maintainer rather than calling it clean myself. Facts for whoever signs off: author is @clubanderson (repository owner); the change adds static error documents and two status reflections to the error backend; no image, resource limits, RBAC, Service, or probe changes.

β€” hive: agent=reviewer-2 backend=copilot model=claude-fable-5.1 copilot=1.0.88

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style / conventions review (read at head eb92f68)

The 403/404 branches follow the existing 401 shape exactly (error_page β†’ internal locations β†’ return in location /), the whole hive-error-pages ConfigMap is mounted without items: (src/deploy/k8s/error-pages.yaml:298-300), so the new keys land automatically. One convention gap:

Add a test pin for the 403/404 branches (in-scope)

Every status branch in this backend is pinned by a Go test that reads the manifest: 502/503 in src/pkg/hub/error_pages_backend_test.go:110-114 (#4241), 401 in src/pkg/hub/saas_api_xhr_ingress_test.go:165-193 (#7405). This PR adds error_page 403 404 /notfound.$hive_error_ext (error-pages.yaml:220) and return 403 / return 404 (error-pages.yaml:239-240) with no pin, while #17431 and the PR body call it "the safety net … so an intercepted 404 can never again become a 503". In this repo "never again" is a test; without one a future cleanup of location / would silently regress to return 503 (error-pages.yaml:246). Suggested addition to src/pkg/hub/error_pages_backend_test.go, mirroring the 401 pin:

+// TestErrorBackendReflectsNotFound pins #17431: an intercepted 403 or 404 must
+// come back as that status with the not-found documents, never the 503
+// "Hive is restarting" fallback (which Search Console reported as Soft 404).
+func TestErrorBackendReflectsNotFound(t *testing.T) {
+	docs, conf := errorPagesConfigMaps(t)
+	directives := stripNginxComments(conf["default.conf"])
+	for _, code := range []string{"403", "404"} {
+		if !regexp.MustCompile(`return\s+` + code + `\b`).MatchString(directives) {
+			t.Errorf("default.conf never returns %s, so an intercepted %s falls through to the 503 fallback (#17431)", code, code)
+		}
+	}
+	if !regexp.MustCompile(`error_page\s+403\s+404\s+/notfound\.\$hive_error_ext`).MatchString(directives) {
+		t.Error("default.conf does not map 403/404 onto the notfound.{json,html} documents")
+	}
+	for _, name := range []string{"notfound.json", "notfound.html"} {
+		if !strings.Contains(directives, "location = /"+name+" { internal; }") {
+			t.Errorf("default.conf has no internal location for /%s", name)
+		}
+	}
+	body, ok := docs["notfound.json"]
+	if !ok {
+		t.Fatal("hive-error-pages ConfigMap has no notfound.json")
+	}
+	var parsed map[string]any
+	if err := json.Unmarshal([]byte(body), &parsed); err != nil {
+		t.Fatalf("notfound.json is not valid JSON: %v", err)
+	}
+	if parsed["ok"] != false {
+		t.Errorf(`notfound.json must report "ok": false, got %v`, parsed["ok"])
+	}
+	if parsed["error"] != "not found" {
+		t.Errorf(`notfound.json "error" = %v, want "not found"`, parsed["error"])
+	}
+	html := docs["notfound.html"]
+	if strings.Contains(html, `http-equiv="refresh"`) {
+		t.Error("notfound.html auto-refreshes; a 404 is not a retry state (#17431)")
+	}
+	if !strings.Contains(html, `name="robots" content="noindex"`) {
+		t.Error("notfound.html lacks noindex; Search Console would index the error page")
+	}
+}

Minor (non-blocking): notfound.html links in #58a6ff (error-pages.yaml:92) where unauthorized.html uses the brand amber #f0b429 (error-pages.yaml:140); and the .bee class now wraps πŸ” rather than a bee (error-pages.yaml:99). Cosmetic only.

β€” hive: agent=ci-maintainer backend=copilot model=claude-fable-5.1 copilot=1.0.88

@hivecommons-hive

hivecommons-hive Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Review follow-ups (below review.backlog.min_severity: P2)

Non-blocking findings below the backlog filing line are listed here instead of as issues, one comment per PR, updated in place. Tick them off as they are fixed; set review.backlog.min_severity: P3 to file every finding.

  • P3 info style β€” notfound.html diverges cosmetically from sibling documents (src/deploy/k8s/error-pages.yaml:92): notfound.html styles links #58a6ff (error-pages.yaml:92) where unauthorized.html uses the brand amber #f0b429 (error-pages.yaml:140), and reuses the .bee class for a πŸ” glyph (error-pages.yaml:9…
  • P3 low docs-currency β€” Global custom-http-errors guidance lives only in an nginx comment, not in an operator doc (src/deploy/k8s/error-pages.yaml:167): The PR records the operational rule 'Keep that global list to "502,503,504"' for the ingress-nginx controller ConfigMap inside the default.conf header comment (src/deploy/k8s/error-pages.yaml:165-171…

@hivecommons-hive

hivecommons-hive Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Hive status β€” updated in place; edits do not notify. Full history is in the Activity Monitor.

Review findings from adjudicator (folded here because this head already carries its hive review):

Correctness review (read at head 391d212, which superseded the dispatched eb92f68).

The nginx mechanics are sound: error_page 403 404 /notfound.$hive_error_ext (src/deploy/k8s/error-pages.yaml:154) preserves the return status, the internal locations at :159-160 are reachable via the internal redirect, and the html volume mounts the whole hive-error-pages ConfigMap with no items: (:299-301), so notfound.{html,json} land on disk. I verified nothing regresses the 401/5xx paths.

1. The new branches are the only ones in this backend without a pinning test (medium)

Every other status this backend reflects is guarded: TestErrorBackendNeverAnswers200 pins return 502/503 (src/pkg/hub/error_pages_backend_test.go:110-114), TestErrorBackendServesJSONToAPICallers pins error.json as valid JSON with ok:false (:148-165), and TestErrorBackendServes401AsJSON pins error_page 401, both documents, and the error key (src/pkg/hub/saas_api_xhr_ingress_test.go:161-192). Nothing pins return 403/return 404 (error-pages.yaml:173-174), the error_page 403 404 mapping, or that notfound.json (:67) is valid JSON with ok:false β€” so the contract of #17431 ("an intercepted 404 can never again become a 503") can regress silently, and a typo in notfound.json would hand API callers an unparseable body. Mirror of the 401 test, appended to error_pages_backend_test.go:

+// TestErrorBackendReflects404NotAs503 pins #17431: an intercepted 403 or 404
+// must come back as that status with the not-found documents, never as the
+// 503 "restarting" page that Search Console reported as a Soft 404.
+func TestErrorBackendReflects404NotAs503(t *testing.T) {
+	docs, conf := errorPagesConfigMaps(t)
+	directives := stripNginxComments(conf["default.conf"])
+	for _, code := range []string{"403", "404"} {
+		if !regexp.MustCompile(`return\s+` + code + `\b`).MatchString(directives) {
+			t.Errorf("default.conf never returns %s; an intercepted %s falls through to 503 (#17431)", code, code)
+		}
+	}
+	if !regexp.MustCompile(`error_page\s+403\s+404\s+/notfound\.\$hive_error_ext`).MatchString(directives) {
+		t.Error("default.conf does not map 403/404 onto the notfound.{json,html} documents")
+	}
+	for _, name := range []string{"notfound.json", "notfound.html"} {
+		if _, ok := docs[name]; !ok {
+			t.Errorf("hive-error-pages ConfigMap has no %s", name)
+		}
+	}
+	var parsed map[string]any
+	if err := json.Unmarshal([]byte(docs["notfound.json"]), &parsed); err != nil {
+		t.Fatalf("notfound.json is not valid JSON: %v", err)
+	}
+	if parsed["ok"] != false {
+		t.Errorf(`notfound.json must report "ok": false, got %v`, parsed["ok"])
+	}
+	if html := docs["notfound.html"]; strings.Contains(html, `http-equiv="refresh"`) || !strings.Contains(html, `name="robots" content="noindex"`) {
+		t.Error("notfound.html must not auto-refresh and must carry noindex (Soft 404, #17431)")
+	}
+}

2. Advisory: a reflected 403 tells API callers "error":"not found" (low)

notfound.json (error-pages.yaml:67) is served for both 403 and 404 (:154). The upstream's 403 body β€” e.g. the "unmanaged repo" JSON the header comment at :122-124 describes β€” is replaced with "error":"not found", so a client keying off error cannot tell forbidden from missing. Per #17431 the shared document is the intended design and 403 is no longer in the global list, so this only matters if the list grows again; not blocking. If you want it exact, a one-line forbidden.json plus error_page 403 /forbidden.$hive_error_ext; is enough.

Evidence bundle `#17432391d212` sha256:b54bfd6a43b88d1a88a934a603faa00a52e0731d0f2d7c175415ae61fb3e1e4a β€” download from the dashboard or `hivectl review evidence #17432`.

@hivecommons-hive
hivecommons-hive Bot merged commit 1b8bfe9 into v5 Nov 9, 2026
82 of 83 checks passed
@kubestellar-prow
kubestellar-prow Bot deleted the fix/error-pages-reflect-404 branch November 9, 2026 21:03
@hivecommons-hive

Copy link
Copy Markdown
Contributor

merged by Hive trusted-author auto-merge: author @clubanderson holds role owner and GitHub permission admin on repo; CI green at 391d212.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Thank you for your contribution! Your PR has been merged.

We'd love to hear how your experience was: share feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

1 participant